/*
 *   Copyright 2012, Thomas Kerber
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package milk.jpatch;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;

import milk.jpatch.fileLevel.ClassPatch;
import milk.jpatch.fileLevel.FilesPatch;

/**
 * Manages command-line and API calls.
 * 
 * @author Thomas Kerber
 * @version 1.1.1
 */
public class JPatch{
    
    /**
     * The version info as printed by the --version flag.
     */
    public static final String VERSION_INFO =
            "JPatch 1.0.0-a1\n\n" +
            
            "Licenced under the Apache Licence Version 2.0.\n" +
            "Copyright 2012 Thomas Kerber";
    
    /**
     * The usage syntax as printed by the --help flag.
     */
    public static final String SYNTAX =
            "java -jar jpatch.jar [options] -{cap} [files...]\n";
    
    /**
     * The JPatch logger. Reference to the Util logger.
     */
    public static final Logger logger = Util.logger;
    
    /**
     * Creates and immediately applies a patch.
     * @param orig The original file.
     * @param mod The modified file.
     * @param in The file to patch.
     * @param out Where to output the patched file.
     * @throws IOException
     */
    public static void generateAndApply(String orig, String mod, String in,
            String out) throws IOException{
        // The original file is considered determining whether zip/jar patch
        // is used or class patch is used.
        String lowerOrig = orig.toLowerCase();
        if(lowerOrig.endsWith(".jar") || lowerOrig.endsWith(".zip"))
            generateAndApplyJarp(orig, mod, in, out);
        else if(lowerOrig.endsWith(".class"))
            generateAndApplyClass(orig, mod, in, out);
        else
            throw new IOException(
                    "Could not determine patch type to use (class or jar).");
    }
    
    /**
     * Creates and immediately applies a JCP.
     * @param origClass The original class.
     * @param modClass The modified class.
     * @param inClass The class to patch.
     * @param outClass Where to output the patched class.
     * @throws IOException
     */
    public static void generateAndApplyClass(String origClass, String modClass,
            String inClass, String outClass) throws IOException{
        ClassPatch.generate(new File(origClass), new File(modClass), "-").
                patch(new File(inClass), new File(outClass));
    }
    
    /**
     * Creates and immediately applies a jarp.
     * @param origZip The original jar.
     * @param modZip The modified jar to diff.
     * @param inZip The jar to patch.
     * @param outZip Where to output the patched jar.
     * @throws IOException
     */
    public static void generateAndApplyJarp(String origZip, String modZip,
            String inZip, String outZip) throws IOException{
        File rootOrig = Util.extractZip(new File(origZip));
        File rootMod = Util.extractZip(new File(modZip));
        FilesPatch fp = FilesPatch.generate(rootOrig, rootMod);
        File rootIn = Util.extractZip(new File(inZip));
        File rootOut = Util.getTempDir();
        fp.patchAll(rootIn, rootOut);
        Util.packZip(new File(outZip), rootOut);
        Util.remDir(rootOrig);
        Util.remDir(rootMod);
        Util.remDir(rootIn);
        Util.remDir(rootOut);
    }
    
    /**
     * Applies a series of patches to a file.
     * @param in The input file.
     * @param out Where to output the finished file.
     * @param mods The mods to apply in order.
     * @throws IOException
     */
    public static void apply(String in, String out, String[] mods)
            throws IOException{
        // The input file is considered determining whether zip/jar or class
        // patch is used.
        String lowerIn = in.toLowerCase();
        if(lowerIn.endsWith(".jar") || lowerIn.endsWith(".zip"))
            applyJarps(in, out, mods);
        else if(lowerIn.endsWith(".class"))
            applyClasses(in, out, mods);
        else
            throw new IOException(
                    "Could not determine patch type (jarp or jcp)");
    }
    
    /**
     * Applies JCPs to an input classfile.
     * @param inClass The input classfile.
     * @param outClass Where to output the patched classfile.
     * @param modJCPs The JCPs to apply in order.
     * @throws IOException
     */
    public static void applyClasses(String inClass, String outClass,
            String[] modJCPs) throws IOException{
        File tmpOld = new File(inClass);
        for(int i = 0; i < modJCPs.length; i++){
            File tmpNew = i == modJCPs.length - 1 ?
                    new File(outClass) :
                    File.createTempFile("milk", ".class");
            ClassPatch.deserializeAt(null, new File(modJCPs[i])).
                    patch(tmpOld, tmpNew);
            if(i != 0)
                tmpOld.delete();
            tmpOld = tmpNew;
        }
    }
    
    /**
     * Applies jarps to an input jar.
     * @param inZip The input jar.
     * @param outZip Where to output the patches jar.
     * @param modJarps The jarps to apply in order.
     * @throws IOException
     */
    public static void applyJarps(String inZip, String outZip,
            String[] modJarps) throws IOException{
        File[] modRoots = new File[modJarps.length];
        for(int i = 0; i < modJarps.length; i++)
            modRoots[i] = Util.extractZip(new File(modJarps[i]));
        FilesPatch[] fps = new FilesPatch[modRoots.length];
        for(int i = 0; i < modRoots.length; i++)
            fps[i] = new FilesPatch(modRoots[i]);
        
        File rootIn = Util.extractZip(new File(inZip));
        File rootOut = Util.getTempDir();
        
        FilesPatch.patchAll(rootIn, rootOut, fps);
        
        Util.packZip(new File(outZip), rootOut);
        for(File f : modRoots)
            Util.remDir(f);
        Util.remDir(rootIn);
        Util.remDir(rootOut);
    }
    
    /**
     * Creates a patch.
     * @param orig The original file.
     * @param mod The modified file.
     * @param out Where to output the patch.
     * @throws IOException
     */
    public static void create(String orig, String mod, String out)
            throws IOException{
        String lowerOrig = orig.toLowerCase();
        if(lowerOrig.endsWith(".jar") || lowerOrig.endsWith(".zip"))
            createJarp(orig, mod, out);
        else if(lowerOrig.endsWith(".class"))
            createJCP(orig, mod, out);
        else
            throw new IOException(
                    "Could not determine patch type to use (class or jar).");
    }
    
    /**
     * Creates a JCP.
     * @param origClass The original class.
     * @param modClass The modified class.
     * @param outClass Where to output the JCP.
     * @throws IOException
     */
    public static void createJCP(String origClass, String modClass,
            String outClass) throws IOException{
        ClassPatch.generate(new File(origClass), new File(modClass), "-").
                dump(outClass);
    }
    
    /**
     * Creates a JARP
     * @param origJar The original jar.
     * @param modJar The modified jar.
     * @param outJar Where to output the JARP.
     * @throws IOException
     */
    public static void createJarp(String origJar, String modJar,
            String outJar) throws IOException{
        File rootOrig = Util.extractZip(new File(origJar));
        File rootMod = Util.extractZip(new File(modJar));
        FilesPatch fp = FilesPatch.generate(rootOrig, rootMod);
        fp.serializeToZip(new File(outJar));
        Util.remDir(rootOrig);
        Util.remDir(rootMod);
    }
    
    /**
     * Creates a JARP, and optionally leaves it as a directory.
     * @param origJar The original jar.
     * @param modJar The modified jar.
     * @param out Where to output the JARP.
     * @param outputAsDir Whether or not the output should be a directory.
     * @throws IOException
     */
    public static void createJarp(String origJar, String modJar, String out,
            boolean outputAsDir) throws IOException{
        File rootOrig = Util.extractZip(new File(origJar));
        File rootMod = Util.extractZip(new File(modJar));
        FilesPatch fp = FilesPatch.generate(rootOrig, rootMod);
        if(outputAsDir)
            fp.serializeToDir(new File(out));
        else
            fp.serializeToZip(new File(out));
        Util.remDir(rootOrig);
        Util.remDir(rootMod);
    }
    
    /**
     * Runs. 'Nuff said.
     * 
     * @param args Command line args.
     */
    public static void main(String[] args){
        CommandLineParser clp = new PosixParser();
        
        Options options = new Options();
        
        OptionGroup force = new OptionGroup();
        force.setRequired(false);
        Option forceClass = new Option(null, "force-class", false,
                "Forces interpretation as a class patch.");
        Option forceJar = new Option(null, "force-jar", false,
                "Forces interpretation as a jar patch");
        force.addOption(forceClass);
        force.addOption(forceJar);
        options.addOptionGroup(force);
        
        OptionGroup actions = new OptionGroup();
        actions.setRequired(true);
        Option create = new Option("c", "create", false,
                "Creates a new patch file. The filenames for the original, " +
                "modified and output files must be passed in that order.");
        Option apply = new Option("a", "apply", false,
                "Applys a patch. The filenames the orginial, output and all " +
                "patch files must be passed in that order. The patch files " +
                "must be passed in the order they are to be applied.");
        Option patch = new Option("p", "patch", false,
                "Creates and immediately applies a patch. The filenames for " +
                "the original, modified, input and output files must be " +
                "passed in that order.");
        actions.addOption(create);
        actions.addOption(apply);
        actions.addOption(patch);
        options.addOptionGroup(actions);
        
        Option help = new Option("h", "help", false,
                "Prints usage information.");
        options.addOption(help);
        Option version = new Option(null, "version", false,
                "Print version information.");
        options.addOption(version);
        
        try{
            CommandLine cl = clp.parse(options, args);
            if(cl.hasOption("h")){
                throw new ParseException("Dummy to generate help message.");
            }
            if(cl.hasOption("version")){
                System.out.println(VERSION_INFO);
                System.exit(0);
            }
            String[] left = cl.getArgs();
            if(cl.hasOption("c")){
                if(left.length < 3)
                    throw new ParseException("Not enough arguments.");
                if(cl.hasOption("force-class"))
                    createJCP(left[0], left[1], left[2]);
                else if(cl.hasOption("froce-jar"))
                    createJarp(left[0], left[1], left[2]);
                else
                    create(left[0], left[1], left[2]);
            }
            else if(cl.hasOption("a")){
                if(left.length < 3)
                    throw new ParseException("Not enough arguments.");
                String[] mods = Arrays.copyOfRange(left, 2, left.length);
                if(cl.hasOption("force-class"))
                    applyClasses(left[0], left[1], mods);
                else if(cl.hasOption("force-jar"))
                    applyJarps(left[0], left[1], mods);
                else
                    apply(left[0], left[1], mods);
            }
            else{ // if(cl.hasOption("p"))
                if(left.length < 4)
                    throw new ParseException("Not enough arguments.");
                if(cl.hasOption("force-class"))
                    generateAndApplyClass(left[0], left[1], left[2], left[3]);
                else if(cl.hasOption("force-jar"))
                    generateAndApplyJarp(left[0], left[1], left[2], left[3]);
                else
                    generateAndApply(left[0], left[1], left[2], left[3]);
            }
        }
        catch(ParseException e){
            new HelpFormatter().printHelp(SYNTAX, options);
        }
        catch(IOException e){
            logger.log(Level.SEVERE, "Top-level IOException:", e);
            System.out.println("Uh-oh... Something went horribly wrong " +
                    "(or you just made a silly mistake). Check your log for " +
                    "details.");
            System.exit(1);
        }
    }
    
}
